package com.csthink.auditlog.config.builder;

import com.csthink.auditlog.EventHelper;
import com.csthink.auditlog.config.parser.AuditLogDetailParser;
import com.csthink.auditlog.domain.AuditLog;
import com.csthink.auditlog.domain.AuditLogEvent;
import com.csthink.auditlog.domain.Change;
import com.csthink.auditlog.enums.ActionEnum;
import com.csthink.auditlog.enums.CategoryEnum;
import com.csthink.auditlog.enums.OperatorType;
import com.csthink.common.infrastructure.enums.Source;
import com.csthink.common.infrastructure.util.SpringUtil;
import com.google.common.base.Strings;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Not thread safe, You should always get a new BaseAuditLogBuilder with
 * specific static method, BaseAuditLogBuilder create();
 *
 * @author <a href="mailto:security.2009@live.cn">Mars</a>
 * @since 2023-09-12
 */
public class BaseAuditLogBuilder {

    public static final String CST_TRACKING_ID = "x-cst-trackingid";

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private static final Logger logger0 = LoggerFactory.getLogger(BaseAuditLogBuilder.class);

    protected AuditLog auditLog = new AuditLog();

    private boolean hasBuild = false;

    private List<Change> changes = new ArrayList<>();
    private List<ChangeBuilder> changeBuilders = new ArrayList<>();

    protected BaseAuditLogBuilder() {}

    public BaseAuditLogBuilder baseOn(AuditLog base) {
        this.auditLog = base;
        return this;
    }

    /**
     * If you use this Base builder, You must invoke this method.
     *
     * It will throw any exception cause that will block business logic.
     */
    public BaseAuditLogBuilder source(Source source) {
        if (source != null) {
            this.auditLog.setSource(source.name());
        }
        return this;
    }

    /**
     * If build is not in http request thread, You should invoke this method.
     */
    public BaseAuditLogBuilder operatorId(String operatorId) {
        this.auditLog.setOperatorId(operatorId);
        return this;
    }

    /**
     * If build is not in http request thread, You must invoke this method.
     *
     * It will throw any exception cause that will block business logic.
     */
    public BaseAuditLogBuilder operatorInfo(String operatorInfo) {
        this.auditLog.setOperatorInfo(operatorInfo);
        return this;
    }

    /**
     * If build is not in http request thread, You should invoke this method.
     */
    public BaseAuditLogBuilder operatorType(OperatorType operatorType) {
        if (operatorType != null) {
            this.auditLog.setOperatorType(operatorType.name());
        }
        return this;
    }

    /**
     * If build is not in http request thread, You should invoke this method.
     */
    public BaseAuditLogBuilder accountId(String accountId) {
        this.auditLog.setAccountId(accountId);
        return this;
    }

    public BaseAuditLogBuilder accountInfo(String accountInfo) {
        this.auditLog.setAccountInfo(accountInfo);
        return this;
    }

    public BaseAuditLogBuilder category(CategoryEnum category) {
        if (category != null) {
            this.auditLog.setCategory(category.getName());
        }
        return this;
    }

    public BaseAuditLogBuilder action(ActionEnum action) {
        if (action != null) {
            this.auditLog.setAction(action.getName());
        }
        return this;
    }

    public BaseAuditLogBuilder actionName(String actionName) {
        if(StringUtils.isNotEmpty(actionName)){
            this.auditLog.setAction(actionName);
        }
        return this;
    }

    public BaseAuditLogBuilder subaction(String subaction) {
        this.auditLog.setSubAction(subaction);
        return this;
    }

    public BaseAuditLogBuilder detail(String detail) {
        this.auditLog.setDetail(detail);
        return this;
    }

    public BaseAuditLogBuilder addChange(Change change) {
        this.changes.add(change);
        return this;
    }

    /**
     * Add one entity. You can add more.
     */
    public BaseAuditLogBuilder addChange(String type, String id, String keyword, Object oldVal, Object newVal, String... fields) {
        this.changeBuilders.add(new ChangeBuilder(type, id, keyword, oldVal, newVal, fields));
        return this;
    }

    public BaseAuditLogBuilder remoteAddr(String remoteAddr){
        this.auditLog.setRemoteAddr(remoteAddr);
        return this;
    }

    public BaseAuditLogBuilder url(String url){
        this.auditLog.setUrl(url);
        return this;
    }

    public BaseAuditLogBuilder startAt(Date time) {
        this.auditLog.setOpStartTime(time);
        return this;
    }

    public BaseAuditLogBuilder endAt(Date time) {
        this.auditLog.setOpEndTime(time);
        return this;
    }

    public AuditLog build() {
        if (!hasBuild) {
            handleSource();
            try {
                handleAccountInfo();
                handleOperatorInfo();
            } catch (Exception e) {
                logger0.error("Fail to handle account & operator info as {}", e);
            }
            handleChanges();
            handleHttpRequestInfo();
            handleTraceId();
            this.auditLog.setCreateTime(new Date());
            this.auditLog.setLogTime(new Date());
            if (StringUtils.isEmpty(this.auditLog.getDetail())) {
                AuditLogDetailParser parser = SpringUtil.getBean(AuditLogDetailParser.class);
                this.auditLog.setDetail(parser.parse(this.auditLog));
            }
            this.hasBuild = true;
        }
        return this.auditLog;
    }

    /**
     * Publish audit log event to spring.
     */
    public void publish() {
        EventHelper.publishEvent(new AuditLogEvent(build()));
    }


    /**
     * cci web, cci api, cci op use different authority method, so it should be use
     * specific way to get user info.
     */
    protected void handleOperatorInfo() {}

    protected void handleAccountInfo() {}

    protected void handleSource() {}

    protected void handleTraceId(){
        String traceId = MDC.get(CST_TRACKING_ID);
        if(StringUtils.isBlank(traceId)) {
            traceId = MDC.get("traceId");
        }
        this.auditLog.setTraceId(traceId);
    }

    /**
     * Just read request.remoteAddr, request.requestURI from current thread HTTP Request.
     * Or else, we should change all controller's method to inject HTTP-Request
     * object.
     *
     * For now, use this.
     */
    protected void handleHttpRequestInfo() {
        HttpServletRequest request = getCurrentRequest();
        if (request != null) {
            try {
                if(Strings.isNullOrEmpty(this.auditLog.getRemoteAddr())) {
                    this.auditLog.setRemoteAddr(request.getRemoteAddr());
                }
                if(Strings.isNullOrEmpty(this.auditLog.getUrl())) {
                    String url = request.getRequestURI();
                    if (StringUtils.isNotBlank(request.getQueryString()))
                        url += "?" + request.getQueryString();
                    this.auditLog.setUrl(url);
                }
            }catch (Exception e){
                logger0.warn("handleHttpRequestInfo error",e);
            }
        }
    }

    protected HttpServletRequest getCurrentRequest() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes instanceof ServletRequestAttributes) {
            return ((ServletRequestAttributes) requestAttributes).getRequest();
        }
        return null;
    }

    /**
     * find differences between old & new value, set them in Change.
     */
    private void handleChanges() {
        for (int i = 0; i < this.changeBuilders.size(); i++) {
            this.changes.add(this.changeBuilders.get(i).build());
        }
        this.auditLog.setChanges(this.changes);
    }
}
